﻿using System;
using System.Collections;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;

namespace MvcExtensions
{
    public static class KOViewModelBuilder
    {
        public static readonly ConcurrentDictionary<string, StringBuilder> KnockoutViewModelDictionary =
            new ConcurrentDictionary<string, StringBuilder>();

        const string AsciiTabs = "\t";
        const string AsciiNewlineTabs = "\n\t";
        private const string Self = "self";

        static KOViewModelBuilder()
        {
            var globalScript = new StringBuilder();
            globalScript.Append(string.Format("{0}function jsonDateToDate(jsonDate){{{0}}}", AsciiNewlineTabs));
            KnockoutViewModelDictionary.TryAdd("GlobalScript", globalScript);
        }

        public static void BuildViewModel(Type type, int noOfTabs = 1)
        {
            if (type == null || KnockoutViewModelDictionary.ContainsKey(type.Name))
                return;

            var properties = GetOrderedProperties(type);

            var modelScript = new StringBuilder();
            var modelUpdateScript = new StringBuilder();
            var addRemoveScript = new StringBuilder();

            var classNewLineTabs = string.Format("\n{0}", AsciiTabs.PadLeft(noOfTabs, '\t'));
            var memberNewLineTabs = string.Format("\n{0}", AsciiTabs.PadLeft(noOfTabs + 1, '\t'));

            modelScript.Append(string.Format("{0}var {1} = function(){{", classNewLineTabs, type.Name));
            modelScript.Append(string.Format("{0}var {1} = this;", memberNewLineTabs, Self));

            modelUpdateScript.Append(string.Format("{0}{1}.Update = function (data){{", memberNewLineTabs, Self));

            foreach (var pinfo in properties)
            {
                if (!pinfo.PropertyType.IsValueType && pinfo.PropertyType != typeof(string))
                {
                    //classes or interface
                    if (typeof(IEnumerable).IsAssignableFrom(pinfo.PropertyType))
                    {
                        if (pinfo.PropertyType.IsGenericType)
                        {
                            var genericParamType = pinfo.PropertyType.GenericTypeArguments[0];
                            BuildViewModel(genericParamType, noOfTabs);
                            modelScript.Append(GetKoArrayProperty(pinfo, genericParamType.Name, noOfTabs + 1));

                            modelUpdateScript.Append(GetKoArrayPropertyUpdate(pinfo, genericParamType.Name, noOfTabs + 2));

                            addRemoveScript.Append(GetAddRemoveScript(pinfo, genericParamType.Name, noOfTabs + 1));
                        }
                        else
                        {
                            modelScript.Append(GetKoArrayProperty(pinfo, noOfTabs: noOfTabs + 1));
                        }
                    }
                    else
                    {
                        BuildViewModel(pinfo.PropertyType, noOfTabs);
                        modelScript.Append(GetKoClassProperty(pinfo, noOfTabs + 1));
                        modelUpdateScript.Append(GetKoClassPropertyUpdate(pinfo, noOfTabs + 2));
                    }
                }
                else
                {
                    modelScript.Append(GetKoProperty(pinfo, noOfTabs + 1));
                    modelUpdateScript.Append(GetKoPropertyUpdate(pinfo, noOfTabs + 2));
                }
            }

            modelUpdateScript.Append(memberNewLineTabs + "}");

            modelScript.Append(modelUpdateScript);

            modelScript.Append(addRemoveScript);

            modelScript.Append(classNewLineTabs + "}\n");

            KnockoutViewModelDictionary.TryAdd(type.Name, modelScript);
        }

        private static IEnumerable<PropertyInfo> GetOrderedProperties(Type type)
        {
            var properties = type.GetProperties();
            var orderedList = properties
                .Select(pinfo => new OrderedPropertyInfo
                {
                    PropertyInfo = pinfo,
                    Order =
                        !pinfo.PropertyType.IsValueType && pinfo.PropertyType != typeof(string)
                            ? (typeof(IEnumerable).IsAssignableFrom(pinfo.PropertyType) ? 1 : 2)
                            : 3
                })
                .ToList();

            return orderedList.OrderBy(x => x.Order).Select(x => x.PropertyInfo).ToList();
        }

        internal class OrderedPropertyInfo
        {
            public int Order { get; set; }
            public PropertyInfo PropertyInfo { get; set; }
        }

        private static string GetKoProperty(PropertyInfo pinfo, int noOfTabs = 1)
        {
            const string koPropertyName = "\n{0}{1}.{2} = ko.observable({3});";

            var defaultValue = GetKoDefaultValue(pinfo);

            return string.Format(koPropertyName, AsciiTabs.PadLeft(noOfTabs, '\t'), Self, pinfo.Name, defaultValue);
        }

        private static string GetKoClassProperty(PropertyInfo pinfo, int noOfTabs = 1)
        {
            const string koPropertyName = "\n{0}{1}.{2} = new {3}();";

            return string.Format(koPropertyName, AsciiTabs.PadLeft(noOfTabs, '\t'), Self, pinfo.Name,
                                 pinfo.PropertyType.Name);
        }

        private static string GetKoArrayProperty(PropertyInfo pinfo, string className = "Object", int noOfTabs = 1)
        {
            const string koPropertyName = "\n{0}{1}.{2} = ko.observableArray([new {3}()])";

            return string.Format(koPropertyName, AsciiTabs.PadLeft(noOfTabs, '\t'), Self, pinfo.Name, className);
        }

        private static string GetKoPropertyUpdate(PropertyInfo pinfo, int noOfTabs = 1)
        {
            const string koPropertyName = "\n{0}{1}.{2}((data == null? '' : (data.{2} == null ? '': data.{2})));";

            return string.Format(koPropertyName, AsciiTabs.PadLeft(noOfTabs, '\t'), Self, pinfo.Name);
        }

        private static string GetKoClassPropertyUpdate(PropertyInfo pinfo, int noOfTabs = 1)
        {
            const string koPropertyName = "\n{0}{1}.{2}.Update(data.{2});";

            return string.Format(koPropertyName, AsciiTabs.PadLeft(noOfTabs, '\t'), Self, pinfo.Name);
        }

        private static string GetKoArrayPropertyUpdate(PropertyInfo pinfo, string className, int noOfTabs = 1)
        {
            const string updateLoopScript = @"
{0}{1}.{2}.removeAll();

{0}if(data != null && data.{2} != null){{
{0}    for (var i=0; i<data.{2}.length; i++){{
        {0}var obsObj = new {3}();
        {0}obsObj.Update(data.{2}[i]);
        {0}{1}.{2}.push(obsObj);
{0}    }}

{0}}}";
            return string.Format(updateLoopScript, AsciiTabs.PadLeft(noOfTabs, '\t'), Self, pinfo.Name, className);
        }

        private static string GetAddRemoveScript(PropertyInfo pinfo, string className, int noOfTabs = 1)
        {
            const string addRemoveScript = @"
{0}{1}.add{3} = function(){{
        {0}{1}.{2}.push(new {3}());
{0}}};
{0}{1}.remove{3} = function(line){{
        {0}{1}.{2}.remove(line);
{0}}};";
            return string.Format(addRemoveScript, AsciiTabs.PadLeft(noOfTabs, '\t'), Self, pinfo.Name, className);

        }

        private static string GetKoDefaultValue(PropertyInfo pinfo)
        {
            if (pinfo.PropertyType == typeof(string))
                return "''";
            if (pinfo.PropertyType == typeof(DateTime))
                return "new Date()";
            if (pinfo.PropertyType == typeof(bool))
                return "false";
            return pinfo.PropertyType.IsValueType
                       ? Activator.CreateInstance(pinfo.PropertyType).ToString()
                       : string.Empty;
        }
    }
}
